import { normalizeBase, stripBase } from './utils'

import type {
  HistoryLocation,
  StateEntry,
  ValueContainer,
  NavigationCallback,
  RouterHistory
} from './types'
import type { HistoryState } from 'vue-router'

/**
 * 该方法主要就是根据当前的 URL（location）以及基础路径（base）创建一个表示当前路由位置的字符串
 * 例如：
 * const base = '/app#'
 * const location = {
 *   pathname: '/app',
 *   search: '',
 *   hash: '#/home',
 * }
 * const currentLocation = createCurrentLocation(base, location)
 * console.log(currentLocation) // 输出 "/home"
 * @param base
 */
function createCurrentLocation(base: string): string {
  const { pathname, search, hash } = window.location
  // 接下来寻找 base 中 # 出现的位置
  const hasPos = base.indexOf('#')
  // 这里会存在两种情况
  // 1. base 里面存在 #
  if (hasPos > -1) {
    // 进入此 if，说明 base 中是存在 # 的
    // 这行代码在判断当前的 hash 是否包含基础路径中 # 之后的部分
    // 如果包含，那么 slicePos 就是基础路径中 # 之后部分的长度
    // 否则 slicePos 就是 1
    const slicePos = hash.includes(base.slice(hasPos)) ? base.slice(hasPos).length : 1
    // 从 hash 中提取路径，这个路径是去掉了 # 和基础路径中 # 之后部分的结果
    let pathFromHash = hash.slice(slicePos)
    // 接下来看一下这个部分的第一个字符如果不是以 / 开头，那么我们就在前面拼接一个 /
    if (pathFromHash[0] !== '/') pathFromHash = '/' + pathFromHash
    return stripBase(pathFromHash, '')
  }
  // 没有进入上面的 if，说明是不需要处理 # 的
  const path = stripBase(pathname, base)
  return path + search + hash
}

/**
 * 该函数的作用时创建一个路由状态对象
 * 例如：
 * const back = '/about'
 * const current = '/home'
 * const forward = '/contact'
 * const state = buildState(back, current, forward)
 * console.log(state)
 * 输出：
 * {
 *  back: "/about",
 *  current: "/home",
 *  forward: "/contact",
 *  replace: false,
 *  position: window.history.length,
 *  scroll: null
 * }
 * @param back 表示从当前路由回退时会进入的路由位置
 * @param current 表示当前的路由位置
 * @param forward 表示从当前路由前进时会进入的路由位置
 * @param replace 这是一个布尔值，表示当前的路由是否通过 router.replace 方法来进行导航
 * @param computedScroll 处理滚动，也是一个布尔值
 */
function buildState(
  back: HistoryLocation | null,
  current: HistoryLocation,
  forward: HistoryLocation | null,
  replace: boolean = false,
  computedScroll: boolean = false
) {
  return {
    back,
    current,
    forward,
    replace,
    scroll: computedScroll ? { left: window.scrollX, top: window.scrollY } : null,
    position: window.history.length - 1
  }
}

// 该方法用于创建导航对象
function useHistoryStateNavigation(base: string) {
  // 1. 搞定 location 信息
  const currentLocation = {
    value: createCurrentLocation(base)
  }

  // 2. 搞定 state 信息
  // 从原生的 history 里面去获取 state
  const historyState = {
    value: window.history.state
  }

  // 看是否是第一次刷新页面
  // 如果是第一次刷新页面，此时是没有任何状态的
  // 我们就需要创建一个初始化状态
  if (!historyState.value) {
    // 说明是第一次
    // 创建状态对象，然后根据状态对象去修改当前的 location
    // 因为是第一次，所以应该 replace 的方式进行初始化导航
    changeLocation(currentLocation.value, buildState(null, currentLocation.value, null, true), true)
  }

  /**
   * 该方法负责根据当前的状态来更新浏览器的 history 历史记录
   * 例如：
   * const base = '/app#'
   * const to = '/home'
   * const state = { foo: 'bar' }
   * changeLocation(to, state, false)
   * console.log(window.location.href) // 输出 "http://example.com/app#/home"
   * console.log(window.history.state) // 输出 "{ foo: 'bar' }"
   * @param to
   * @param state
   * @param replace
   */
  function changeLocation(to: HistoryLocation, state: HistoryState, replace: boolean): void {
    // 查看 base 是否有 #
    const hasPos = base.indexOf('#')
    const url = hasPos > -1 ? base + to : to
    // 接下来进行 state 的更新
    window.history[replace ? 'replaceState' : 'pushState'](state, '', url)
    // 更新本地的 historyState
    historyState.value = state
  }

  // 3. 书写 push 方法
  function push(to: HistoryLocation, data?: HistoryState) {
    // 1. 构建当前的状态对象
    const currentState = Object.assign({}, historyState.value, {
      forward: to,
      scroll: { left: window.scrollX, top: window.scrollY }
    })

    // 2. 改变当前的 URL
    // 本质上没有跳转，只是更新了状态，方便 back 之后在  forward 是正常
    changeLocation(currentState.current, currentState, true)

    // 3. 再次构建新的状态对象
    const state = Object.assign(
      {},
      buildState(currentLocation.value, to, null),
      {
        position: currentState.position + 1
      },
      data
    )

    // 4. 再次 changeLocation
    // 注意这一次就是 push 模式
    changeLocation(to, state, false)

    // 5. 更新当前的路由位置
    currentLocation.value = to
  }

  // 4. 书写 repalce 方法
  function replace(to: HistoryLocation, data?: HistoryState) {
    // 1. 构建新的状态对象
    const state = Object.assign(
      {},
      buildState(historyState.value.back, to, historyState.value.forward, true),
      data
    )
    // 2. 改变当前的 url
    changeLocation(to, state, true)

    // 3. 更新当前的路由位置
    currentLocation.value = to
  }

  return {
    location: currentLocation,
    state: historyState,
    push,
    replace
  }
}

/**
 * 创建一个监听的对象，这个对象会提供一个 listen 方法
 * @param base
 * @param historyState
 * @param currentLocation
 */
function useHistoryListeners(
  base: string,
  historyState: ValueContainer<StateEntry>,
  currentLocation: ValueContainer<HistoryLocation>
) {
  // 1. 定义一个数组，该数组用于存储要执行的回调函数
  const listeners: NavigationCallback[] = []

  const popStateHandler = ({ state }: { state: StateEntry }) => {
    const to = createCurrentLocation(base) // 去哪里
    const from = currentLocation.value // 从哪里来
    const fromState = historyState.value // 来的时候的一个状态

    // 更新最新的状态
    currentLocation.value = to
    historyState.value = state

    // 判断是前进还是后退
    const isBack = state.position - fromState.position < 0

    // 执行 listeners 数组里面所有的回调函数
    listeners.forEach((listener) => {
      listener(to, from, { isBack })
    })
  }

  // 2. 绑定一个 popstate 事件
  // 该事件会在浏览器前进或者后退的时候会被触发
  window.addEventListener('popstate', popStateHandler)

  function listen(cb: NavigationCallback) {
    listeners.push(cb)
  }

  return {
    listen
  }
}

export function createWebHistory(base?: string): RouterHistory {
  // 1. 针对 base 做一个标准化操作
  base = normalizeBase(base)

  // 2. 创建导航对象
  const historyNavigation = useHistoryStateNavigation(base)

  // 3. 创建监听对象
  const historyListeners = useHistoryListeners(
    base,
    historyNavigation.state,
    historyNavigation.location
  )

  // 4. 将上面的对象合并到 routerHistory 的对象里面
  const routerHistory = Object.assign({}, historyNavigation, historyListeners)

  // 5. 为 routerHistory 添加 location 和 state 属性
  Object.defineProperty(routerHistory, 'location', {
    get: () => historyNavigation.location.value
  })
  Object.defineProperty(routerHistory, 'state', {
    get: () => historyNavigation.state.value
  })

  // 6. 返回 routerHistory
  return routerHistory
}

